function deploy {

    #Requires -Version 3.0
    #Requires -Module Az.Resources
    #Requires -Module Az.Storage

    Param(
        [string] [Parameter(Mandatory = $true)] $ArtifactStagingDirectory,
        [string] [Parameter(Mandatory = $true)][alias("ResourceGroupLocation")] $Location,
        [string] $ResourceGroupName = (Split-Path $ArtifactStagingDirectory -Leaf),
        [switch] $UploadArtifacts,
        [string] $StorageAccountName,
        [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
        [string] $TemplateFile = 'mainTemplate.json',
        [string] $TemplateParametersFile = 'azuredeploy.parameters.json',
        [string] $DSCSourceFolder = 'DSC',
        [switch] $BuildDscPackage,
        [switch] $ValidateOnly,
        [string] $DebugOptions = "None",
        [string] $Mode = "Incremental",
        [string] $DeploymentName = ((Split-Path $TemplateFile -LeafBase) + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')),
        [string] $ManagementGroupId,
        [switch] $Dev,
        [switch] $bicep,
        [switch] $whatIf
    )

    try {
        [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("AzQuickStarts-$UI$($host.name)".replace(" ", "_"), "1.0")
    }
    catch { }

    $currentFolder = $(Get-Location).path
    # Convert relative paths to absolute paths if needed
    $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($currentFolder, $ArtifactStagingDirectory))
    $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($currentFolder, $DSCSourceFolder))

    $ErrorActionPreference = 'Stop'
    Set-StrictMode -Version 3

    function Format-ValidationOutput {
        param ($ValidationOutput, [int] $Depth = 0)
        Set-StrictMode -Off
        return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @('  ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) })
    }

    $OptionalParameters = New-Object -TypeName Hashtable
    $TemplateArgs = New-Object -TypeName Hashtable
    $ArtifactStagingDirectory = ($ArtifactStagingDirectory.TrimEnd('/')).TrimEnd('\')

    # if the bicep switch is set, and the templateFile arg was the default, swap .json for .bicep
    $isBicep = ($bicep -or $TemplateFile.EndsWith('.bicep'))
    if ($isBicep) {
        $defaultTemplateFile = 'main.bicep'
    }
    else {
        $defaultTemplateFile = 'azuredeploy.json'
    }

    # if the template file isn't found, try another default
    if (!(Test-Path "$ArtifactStagingDirectory\$TemplateFile")) { 
        $TemplateFile = $defaultTemplateFile
    }

    # build the bicep file
    if ($isBicep) {
        bicep build "$ArtifactStagingDirectory\$TemplateFile"
        # now point the deployment to the json file that was just build
        $TemplateFile = $TemplateFile.Replace('.bicep', '.json')
        $fromBicep = " (from bicep build)"
    }else{
        $fromBicep = ""
    }
    
    Write-Host "Using template file $($fromBicep):  $TemplateFile"
    
    #try a few different default options for param files when the -dev switch is use
    if ($Dev) {
        $TemplateParametersFile = $TemplateParametersFile.Replace('azuredeploy.parameters.json', 'azuredeploy.parameters.dev.json')
        if (!(Test-Path $TemplateParametersFile)) {
            $TemplateParametersFile = $TemplateParametersFile.Replace('azuredeploy.parameters.dev.json', 'azuredeploy.parameters.1.json')
        }
    }

    Write-Host "Using parameter file: $TemplateParametersFile"

    if (!$ValidateOnly) {
        $OptionalParameters.Add('DeploymentDebugLogLevel', $DebugOptions)
        if ($whatIf) {
            $OptionalParameters.Add('WhatIf', $whatIf)
        }
    }

    $TemplateFile           = "$ArtifactStagingDirectory\$TemplateFile"
    $TemplateParametersFile = "$ArtifactStagingDirectory\$TemplateParametersFile"

    $TemplateJSON = Get-Content $TemplateFile -Raw | ConvertFrom-Json

    $TemplateSchema = $TemplateJson | Select-Object -expand '$schema' -ErrorAction Ignore

    switch -Wildcard ($TemplateSchema) {
        '*tenantDeploymentTemplate.json*' {
            $deploymentScope = "Tenant"
        }
        '*managementGroupDeploymentTemplate.json*' {
            $deploymentScope = "ManagementGroup"
        }
        '*subscriptionDeploymentTemplate.json*' {
            $deploymentScope = "Subscription"
        }
        '*/deploymentTemplate.json*' {
            $deploymentScope = "ResourceGroup"
            $OptionalParameters.Add('Mode', $Mode)
            if(!$ValidateOnly -and !$WhatIf) {
                $OptionalParameters.Add('Force', $true)
            }
        }
    }

    Write-Host "Running a $deploymentScope scoped deployment..."

    $ArtifactsLocationName = '_artifactsLocation'
    $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken'
    $ArtifactsLocationParameter = $TemplateJson | Select-Object -expand 'parameters' -ErrorAction Ignore | Select-Object -Expand $ArtifactsLocationName -ErrorAction Ignore
    $ArtifactsLocationSasTokenParameter = $TemplateJson | Select-Object -expand 'parameters' -ErrorAction Ignore | Select-Object -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore
    $useAbsolutePathStaging = $($ArtifactsLocationParameter -ne $null)

    # if the switch is set or the standard parameter is present in the template, upload all artifacts
    if ($UploadArtifacts -Or $useAbsolutePathStaging -or $ArtifactsLocationSasTokenParameter) {

        # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
        if (Test-Path $TemplateParametersFile) {
            $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
            if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) {
                $JsonParameters = $JsonParameters.parameters
            }
        }
        else {
            $JsonParameters = @{ }
        }
    
        # if using _artifacts* parameters, add them to the optional params and get the value from the param file (if any)
        if ($useAbsolutePathStaging) {
            $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select-Object -Expand $ArtifactsLocationName -ErrorAction Ignore | Select-Object -Expand 'value' -ErrorAction Ignore
            $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select-Object -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select-Object -Expand 'value' -ErrorAction Ignore
        }

        # Create DSC configuration archive
        if ((Test-Path $DSCSourceFolder) -and ($BuildDscPackage)) {
            $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process { $_.FullName })
            foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
                $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip'
                Publish-AzVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
            }
        }

        # Create a storage account name if none was provided
        if ($StorageAccountName -eq '') {
            $StorageAccountName = 'stage' + ((Get-AzContext).Subscription.Id).Replace('-', '').substring(0, 19)
        }

        $StorageAccount = (Get-AzStorageAccount | Where-Object { $_.StorageAccountName -eq $StorageAccountName })

        # Create the storage account if it doesn't already exist
        if ($StorageAccount -eq $null) {
            $StorageResourceGroupName = 'ARM_Deploy_Staging'
            if ((Get-AzResourceGroup -Name $StorageResourceGroupName -Verbose -ErrorAction SilentlyContinue) -eq $null) {
                New-AzResourceGroup -Name $StorageResourceGroupName -Location $Location -Verbose -Force -ErrorAction Stop
            }
            $StorageAccount = New-AzStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$Location"
        }

        if ($StorageContainerName.length -gt 63) {
            $StorageContainerName = $StorageContainerName.Substring(0, 63)
        }
        $ArtifactStagingLocation = $StorageAccount.Context.BlobEndPoint + $StorageContainerName + "/"   

        # Generate the value for artifacts location if it is not provided in the parameter file
        if ($useAbsolutePathStaging -and $OptionalParameters[$ArtifactsLocationName] -eq $null) {
            #if the defaultValue for _artifactsLocation is using the template location, use the defaultValue, otherwise set it to the staging location
            $defaultValue = $ArtifactsLocationParameter | Select-Object -Expand 'defaultValue' -ErrorAction Ignore
            if ($defaultValue -like '*deployment().properties.templateLink.uri*') {
                $OptionalParameters.Remove($ArtifactsLocationName) # just use the defaultValue if it's using the template language function
            }
            else {
                $OptionalParameters[$ArtifactsLocationName] = $ArtifactStagingLocation   
            }
        } 

        # Copy files from the local storage staging location to the storage account container
        New-AzStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1

        $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process { $_.FullName }
        foreach ($SourcePath in $ArtifactFilePaths) {
        
            if ($SourcePath -like "$DSCSourceFolder*" -and $SourcePath -like "*.zip" -or !($SourcePath -like "$DSCSourceFolder*")) {
                #When using DSC, just copy the DSC archive, not all the modules and source files
                $blobName = ($SourcePath -ireplace [regex]::Escape($ArtifactStagingDirectory), "").TrimStart("/").TrimStart("\")
                Set-AzStorageBlobContent -File $SourcePath -Blob $blobName -Container $StorageContainerName -Context $StorageAccount.Context -Force
            }
        }

        # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file
        # first check to see if we need a sasToken (if it was not already provided in the param file or we're using relativePath)
        if ($useAbsolutePathStaging -or $OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) {
            $sasToken = (New-AzStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4))
        }

        # now set the parameter value for the QueryString or _artifactsLocationSasToken as appropriate
        if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null -and $useAbsolutePathStaging) {
            $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString $sasToken -AsPlainText -Force
            $TemplateArgs.Add('TemplateUri', $ArtifactStagingLocation + (Get-ChildItem $TemplateFile).Name + $sasToken)
        }
        elseif (!$useAbsolutePathStaging) {
            $OptionalParameters['QueryString'] = $sasToken.TrimStart("?") # remove leading ? as it is not part of the QueryString
            $TemplateArgs.Add('TemplateUri', $ArtifactStagingLocation + (Get-ChildItem $TemplateFile).Name)
        }
    }
    else {

        $TemplateArgs.Add('TemplateFile', $TemplateFile)

    }

    if (Test-Path $TemplateParametersFile) {
        $TemplateArgs.Add('TemplateParameterFile', $TemplateParametersFile)
    }

    Write-Host ($TemplateArgs | Out-String)
    Write-Host ($OptionalParameters | Out-String)

    # Create the resource group only when it doesn't already exist - and only in RG scoped deployments
    if ($deploymentScope -eq "ResourceGroup") {
        if ((Get-AzResourceGroup -Name $ResourceGroupName -Location $Location -Verbose -ErrorAction SilentlyContinue) -eq $null) {
            New-AzResourceGroup -Name $ResourceGroupName -Location $Location -Verbose -Force -ErrorAction Stop
        }
    }

    if ($ValidateOnly) {
    
        switch ($deploymentScope) {
            "resourceGroup" {
                $ErrorMessages = Format-ValidationOutput (Test-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName @TemplateArgs @OptionalParameters)
            }
            "Subscription" {
                $ErrorMessages = Format-ValidationOutput (Test-AzDeployment -Location $Location @TemplateArgs @OptionalParameters)
            }
            "managementGroup" {           
                $ErrorMessages = Format-ValidationOutput (Test-AzManagementGroupDeployment -Location $Location @TemplateArgs @OptionalParameters)
            }
            "tenant" {
                $ErrorMessages = Format-ValidationOutput (Test-AzTenantDeployment -Location $Location @TemplateArgs @OptionalParameters)
            }
        }

        if ($ErrorMessages) {
            Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.'
        }
        else {
            Write-Output '', 'Template is valid.'
        }
    }

    else {

        $ErrorActionPreference = 'Continue' # Switch to Continue" so multiple errors can be formatted and output
    
        switch ($deploymentScope) {
            "resourceGroup" {
                New-AzResourceGroupDeployment -Name $DeploymentName `
                    -ResourceGroupName $ResourceGroupName `
                    @TemplateArgs `
                    @OptionalParameters `
                    -Verbose `
                    -ErrorVariable ErrorMessages
            }
            "Subscription" {
                New-AzDeployment -Name $DeploymentName `
                    -Location $Location `
                    @TemplateArgs `
                    @OptionalParameters `
                    -Verbose `
                    -ErrorVariable ErrorMessages
            }
            "managementGroup" {           
                New-AzManagementGroupDeployment -Name $DeploymentName `
                    -ManagementGroupId $managementGroupId `
                    -Location $Location `
                    @TemplateArgs `
                    @OptionalParameters `
                    -Verbose `
                    -ErrorVariable ErrorMessages
            }
            "tenant" {
                New-AzTenantDeployment -Name $DeploymentName `
                    -Location $Location `
                    @TemplateArgs `
                    @OptionalParameters `
                    -Verbose `
                    -ErrorVariable ErrorMessages
            }
        }
    
        $ErrorActionPreference = 'Stop' 
        if ($ErrorMessages) {
            Write-Output '', 'Template deployment returned the following errors:', '', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message })
            Write-Error "Deployment failed."
        }

    }
}
